package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Shop queryById(Long id) throws InterruptedException {
//        Shop shop = queryWithPassThrough(id);
//        Shop shop = queryWithMutex(id);
        Shop shop = queryWithLogicalExpritation(id);
        return shop;
    }

    private ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

//    缓存击穿
//    一个高并发访问且缓存重建时间很长的的热点key突然消失（大量的并发请求，缓存重建时间很长），造成大量请求访问数据库
//    解决方案
//    互斥锁，只允许一个线程访问数据库
//    逻辑过期 热点数据自带一个逻辑过期时间,热点数据永远保存在缓存中
    private Shop queryWithMutex(Long id) {
        Shop shop= null;
        try {
            String key = RedisConstants.CACHE_SHOP_KEY+id;
            String shopJson = stringRedisTemplate.opsForValue().get(key);
            //缓存命中
            if (!StrUtil.isBlank(shopJson)) {
                return JSONUtil.toBean(shopJson, Shop.class);
            }
            if(shopJson != null){
                return null;
            }
            //缓存未命中，尝试获取锁
            //如果没有获取锁，则休眠一段时间后重新查找缓存
            if(!tryLock(id))
            {
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //双重检测，防止另一个线程获取锁之后再重建缓存
            //缓存命中
            if (!StrUtil.isBlank(shopJson)) {
                return JSONUtil.toBean(shopJson, Shop.class);
            }
            if(shopJson != null){
                return null;
            }
            //如果获取锁，则查询数据库
            shop = getById(id);
            //模拟重建缓存的时间很长
            Thread.sleep(200);
            //数据库也未命中，缓存一个空值，防止缓存穿透
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //数据库命中，缓存数据
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            unLock(id);
        }
        return shop;
    }

    private Shop queryWithLogicalExpritation(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY+id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //缓存未命中
        if (StrUtil.isBlank(shopJson)) {
            return null;
        }
        //缓存命中，判断缓存是否过期
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        //缓存未过期，直接返回缓存数据
        if(expireTime.isAfter(LocalDateTime.now()))
            return shop;
        //缓存过期，单独开一个线程来重建缓存
        if(tryLock(id))
        {
            //双重检测，防止另一个线程获取锁之后再重建缓存
            shopJson = stringRedisTemplate.opsForValue().get(key);
            redisData = JSONUtil.toBean(shopJson, RedisData.class);
            expireTime = redisData.getExpireTime();
            shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
            if(expireTime.isAfter(LocalDateTime.now()))
                return shop;
            //重建缓存
            CACHE_REBUILD_EXECUTOR.execute(() -> {
                try {
                    saveShop2Redis(id,20L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }finally {
                    unLock(id);
                }
            });
        }
        return shop;
    }

//    缓存穿透
//    请求的数据既不在缓存中，也不在数据库中。每次请求都会直接穿透缓存，打到数据库。
//    解决方案
//    (1) 布隆过滤器
//    在缓存层之前加一个布隆过滤器。如果布隆过滤器判断key不存在，则直接返回，不再查询数据库。
//    (2) 缓存空值
//    即使数据库查询结果为空，也缓存一个空值，并设置较短的过期时间。避免每次查询都穿透到数据库
    private Shop queryWithPassThrough(Long id) throws InterruptedException {
        String key = RedisConstants.CACHE_SHOP_KEY+id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //缓存命中
        if (!StrUtil.isBlank(shopJson)) {
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        if(shopJson != null){
            return null;
        }
        //缓存未命中，查询数据库，并更新缓存
        Shop shop = getById(id);
        Thread.sleep(500);
        //数据库也未命中，缓存一个空值，防止缓存穿透
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }

    @Override
    @Transactional
    //缓存更新策略:
    //在更新数据库的时候删除缓存，在查询的时候更新缓存.而不是在更新数据库的时候更新缓存，这样做可以减少无效的写操作
    public Object updateShop(Shop shop) {
        //先更新数据库，再删除缓存
        updateById(shop);
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY+shop.getId());
        return null;
    }

    private boolean tryLock(Long id)
    {
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(RedisConstants.LOCK_SHOP_KEY+id, "lock", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(lock);
    }

    private void unLock(Long id)
    {
        stringRedisTemplate.delete(RedisConstants.LOCK_SHOP_KEY+id);
    }

    public void saveShop2Redis(Long id,Long expireSeconds) throws InterruptedException {
        Thread.sleep(200);
        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }
}
